package com.hmdp.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.entity.ShopType;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisData;
import com.hmdp.utils.SystemConstants;
import net.bytebuddy.utility.dispatcher.JavaDispatcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisCommands;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.awt.*;
import java.time.LocalDateTime;
import java.util.*;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.hmdp.utils.RedisConstants.*;
import static com.hmdp.utils.SystemConstants.DEFAULT_PAGE_SIZE;
import static com.hmdp.utils.SystemConstants.MAX_PAGE_SIZE;


@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private CacheClient cacheClient;

    //创建线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 根据id查店铺信息
     *
     * @param id
     * @return
     */

    //未优化查询缓存
    /*@Override
    public Result queryById(Long id){
        //1.构建key并查询缓存
        String key = CACHE_SHOP_KEY + id;
        String jsonShop = stringRedisTemplate.opsForValue().get(key);

        //2.判断是否为空
        //2.1 缓存不为空
        if(StrUtil.isNotBlank(jsonShop)){
            //反序列化为shop对象直接返回
            Shop shop = JSONUtil.toBean(jsonShop, Shop.class);
            return Result.ok(shop);
        }

        //2.2缓存为空,查询数据库
        Shop shop = this.getById(id);
        if(shop==null){
            return  Result.fail("店铺不存在");
        }
        //2.3数据库存在，写入缓存
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
        return Result.ok(shop);

    }*/
    @Override
    public Result queryById(Long id) {
        //解决缓存穿透
        //Shop shop = queryWithPassThrough(id);
        //Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);

        //利用互斥锁解决缓存击穿
        //return queryWithMutex(id);

        //利用逻辑过期时间解决缓存击穿
        Shop shop = queryWithLogicExpireTime(id);
        //Shop shop = cacheClient.queryWithLogicExpireTime(CACHE_SHOP_KEY, id, Shop.class, id2->getById(id2),20L,TimeUnit.SECONDS);
        //Shop shop = cacheClient.queryWithLogicExpireTime(CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.SECONDS);


        if (shop == null) {
            return Result.fail("店铺不存在");
        }
        //返回
        return Result.ok(shop);
    }

    //解决缓存穿透问题
    public Shop queryWithPassThrough(Long id) {
        //查缓存
        String key = CACHE_SHOP_KEY + id;
        String data = stringRedisTemplate.opsForValue().get(key);

        //缓存有信息，直接返回  isNotBlank排除了null和空字符串
        if (StrUtil.isNotBlank(data)) {
            //json字符串转为对象
            Shop shop = JSONUtil.toBean(data, Shop.class);
            return shop;
        }
        if ("".equals(data)) {
            return null;
        }

        //缓存没有查数据库
        Shop shop = this.getById(id);
        //数据库没有数据,缓存空字符串
        if (shop == null) {
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        //写入缓存,对象转为json字符串
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //返回
        return shop;
    }

    //互斥锁解决缓存击穿问题
    public Result queryWithMutex(Long id) {
        //查缓存
        String key = CACHE_SHOP_KEY + id;
        String data = stringRedisTemplate.opsForValue().get(key);

        //缓存有信息，直接返回
        if (StrUtil.isNotBlank(data)) {
            //json字符串转为对象
            Shop shop = JSONUtil.toBean(data, Shop.class);
            return Result.ok(shop);
        }
        if ("".equals(data)) {
            return Result.fail("店铺不存在");
        }

        //缓存没有,尝试获取锁，获取不到休眠一段时间再获取锁
        Shop shop = null;
        try {
            if (!tryGetLock(LOCK_SHOP_KEY + id)) {
                Thread.sleep(50); //后面来的线程等待，递归查询，直到缓存重构后获取锁
                return queryWithMutex(id);
            }

            //获取锁成功，（后面获取锁的线程）判断缓存是否已经重建，防止堆积的线程全部请求数据库
            // 互斥锁已经释放了，缓存已经重建了，没必要查数据库（所以说双检是很有必要的）
            Result shopFromCache = getShopFromCache(key);
            if (Objects.nonNull(shopFromCache)) {
                // 缓存命中(包括不存在)，直接返回
                return shopFromCache;
            }

            //第一个获取到锁的重构缓存，防止后面多个线程打到数据建库
            shop = this.getById(id);
            Thread.sleep(200);
            //数据库没有数据,缓存空字符串，防止缓存穿透
            if (shop == null) {
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return Result.fail("店铺不存在");
            }
            //
            //写入缓存,对象转为json字符串
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);

        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //释放锁
            releaseLock(LOCK_SHOP_KEY + id);
        }

        //返回
        return Result.ok(shop);
    }

    //缓存击穿的双检 封装方法
    private Result getShopFromCache(String key) {
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 判断缓存是否命中
        if (StrUtil.isNotBlank(shopJson)) {
            // 缓存数据有值，说明缓存命中了，直接返回店铺数据
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // 判断缓存中查询的数据是否是空字符串(isNotBlank把 null 和 空字符串 给排除了)
        if (Objects.nonNull(shopJson)) {
            // 当前数据是空字符串，说明缓存也命中了（该数据是之前缓存的空对象），直接返回失败信息
            return Result.fail("店铺不存在");
        }
        // 缓存未命中（缓存数据既没有值，又不是空字符串）
        return null;
    }

    //逻辑过期时间解决缓存击穿
    public Shop queryWithLogicExpireTime(Long id) {
        //查缓存
        String key = CACHE_SHOP_KEY + id;
        String JsonData = stringRedisTemplate.opsForValue().get(key);

        //缓存没有数据，说明不是热点数据(不用解决缓存击穿问题)，直接返回null
        if (StrUtil.isBlank(JsonData)) {
            return null;
        }

        //命中，有数据，把json序列化为对象
        RedisData redisDate = JSONUtil.toBean(JsonData, RedisData.class);

        //JSON：是用于数据传输或存储的字符串格式，广泛应用于网络通信和 API 中。
        //JSONObject：是 Java 中操作 JSON 数据的类，用于解析、修改或创建 JSON 数据，可以认为是 Java 中 JSON 数据的具体实现。

        //Object data = redisDate.getData();  其实是JsonObject，不是类的object，不能强转为shop
        //当通过JSONUtil.toBean(shopJson, RedisData.class) 从 JSON 中反序列化时，
        // data 实际上可能被反序列化为 JSONObject（对象），而不是直接被解析为你期望的 Shop 对象
        //因为JSONUtil 无法直接将 Object 推断为具体的类型。
        JSONObject jsonObject = (JSONObject) redisDate.getData();
        Shop shop = JSONUtil.toBean(jsonObject, Shop.class);
        LocalDateTime expireTime = redisDate.getExpireTime();

        // 判断逻辑过期时间是否过期
        //没有过期，直接返回shop
        if (expireTime.isAfter(LocalDateTime.now())) {
            return shop;
        }

        //过期了，需要缓存重建
        //尝试获得锁
        //如果获得锁，开启另独立线程进行缓存重构
        if (tryGetLock(LOCK_SHOP_KEY + id)) {
            //重新构建 异步
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    saveToCache(id, 20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁  注意释放锁是在异步线程重建后才释放的
                    releaseLock(LOCK_SHOP_KEY);
                }
            });

        }

        // 3、获取锁失败，再次查询缓存，判断缓存是否重建（这里双检是有必要的）
        JsonData = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(JsonData)) {
            // 3.1 缓存未命中，直接返回失败信息
            return null;
        }
        // 3.2 缓存命中，将JSON字符串反序列化未对象
        redisDate = JSONUtil.toBean(JsonData, RedisData.class);
        //JsonObject是一个类
        // 这里需要先转成JSONObject再转成反序列化，否则可能无法正确映射Shop的字段
        jsonObject = (JSONObject) redisDate.getData(); //jsonObject对象
        shop = JSONUtil.toBean(jsonObject, Shop.class);


        return shop;
    }

    //缓存重建(热点key)
    public void saveToCache(Long id, Long expireSecond) throws InterruptedException {
        //根据id查询数据库
        Shop shop = this.getById(id);
        Thread.sleep(200);
        RedisData redisData = new RedisData();
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSecond));
        redisData.setData(shop);
        //缓存重建
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

    //获取互斥锁 setnx
    public boolean tryGetLock(String lock) {
        //如果不存在这个key则设置
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lock, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(result); //不能直接返回，自动拆箱 会空指针
    }



    //释放锁
    public void releaseLock(String lock) {
        stringRedisTemplate.delete(lock);
    }

    //更新数据库，再删除缓存
    @Transactional
    @Override
    public Result update(Shop shop) {
        //更新时，先更新数据库，再删除缓存，可能会出现缓存和数据库不一致的情况（概率不大）

        Long shopId = shop.getId();
        if (shopId == null) {
            return Result.fail("id为空，更新失败");
        }
        //更新数据库
        boolean success = this.updateById(shop);
        if (!success) {
            throw new RuntimeException("数据库更新失败");
        }
        //删除缓存
        success = stringRedisTemplate.delete(CACHE_SHOP_KEY + shopId);
        if (!success) {
            throw new RuntimeException("缓存删除失败");
        }
        return Result.ok("更新成功");
    }

    //根据店铺类型查询附近商铺
    @Override
    public List<Shop> queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        //判断是否需要根据坐标查询
        if (x == null || y == null) {
            //直接分页查询返回
            Page<Shop> page = lambdaQuery().eq(Shop::getTypeId, typeId).page(new Page<>(current, DEFAULT_PAGE_SIZE));
            return page.getRecords();
        }

        //计算分页开始和结束 的第几条数据
        int from = (current - 1) * DEFAULT_PAGE_SIZE;
        int end = current * DEFAULT_PAGE_SIZE;

        String key = SHOP_GEO_KEY + typeId;
        //GeoResults(多个) ->GeoResult(Location,distance)
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo().search(
                key,
                GeoReference.fromCoordinate(x, y),
                new Distance(5000),
                RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs()
                        .includeDistance().limit(end)  //包括距离，限制查询结果不能超过end
        );
        //获取结果集内容 转为list集合
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
        //如果没有下一页了，直接返回空
        //results.getContent().size();
        if (list.size() <= from) {
            return Collections.emptyList();
        }

        //ids集合存储店铺id 方便根据ids查询店铺list
        List<Long> ids = new ArrayList<>(list.size());
        //map集合存储id和距离 方便设置shop的distance字段
        Map<String, Distance> distanceMap = new HashMap<>();

        //解析结果
        //跳过前面元素 遍历并设置id和距离 限制最大条数 提高效率
        list.stream().skip(from).limit(MAX_PAGE_SIZE).forEach(result -> {
            String shopId = result.getContent().getName();
            Distance distance = result.getDistance();
            ids.add(Long.valueOf(shopId));
            distanceMap.put(shopId, distance);
        });

        //解析list 获取店铺id 和 距离
        /*for (GeoResult<RedisGeoCommands.GeoLocation<String>> result : list) {
            //距离该店铺的距离对象
            Distance distance = result.getDistance();

            //包含每一个店铺的位置和id
            RedisGeoCommands.GeoLocation<String> location = result.getContent();
            //member 店铺id
            String shopId = location.getName();
            //店铺的经纬度
            Point point = location.getPoint();
            double longitude = point.getX(); // 经度
            double latitude = point.getY();  // 纬度

            //添加id到集合中
            ids.add(Long.valueOf(shopId));
            //将id和距离转为map集合，方便后续查询店铺并设置店铺距离
            distanceMap.put(shopId, distance);
        }*/

        //根据ids查询店铺
/*        String idStr = StrUtil.join(",", ids);
        List<Shop> shopList = this.lambdaQuery().in(Shop::getId,ids)
                   .last("ORDER BY FIELD(id," + idStr + ")").list();*/
        //String.join只能将集合元素为String类型的添加分隔符
        List<Shop> shopList = this.lambdaQuery().in(Shop::getId, ids)
                .last("ORDER BY FIELD(id," + String.join(",", ids.stream().map(String::valueOf).collect(Collectors.toList())) + ")")
                .list();


        //为每个店铺设置距离
        shopList.stream().forEach(shop -> {
            Distance distance = distanceMap.get(shop.getId().toString());
            if (distance != null) {
                shop.setDistance(distance.getValue());
            }
        });
        return shopList;

    }
}
